/*-------------------------------------------------------------------------
 *
 * hio.c
 *	  POSTGRES 堆访问方法的输入/输出代码。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/heap/hio.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/heapam.h"
#include "access/hio.h"
#include "access/htup_details.h"
#include "access/visibilitymap.h"
#include "storage/bufmgr.h"
#include "storage/freespace.h"
#include "storage/lmgr.h"
#include "storage/smgr.h"


/*
 * RelationPutHeapTuple - 将元组放置在指定页面
 *
 * !!! EREPORT(ERROR) 在这里是不允许的 !!!  失败时必须 PANIC !!!
 *
 * 注意 - 调用者必须在缓冲区上持有 BUFFER_LOCK_EXCLUSIVE。
 */
void RelationPutHeapTuple(Relation fc_relation,
					 Buffer fc_buffer,
					 HeapTuple fc_tuple,
					 bool fc_token)
{
	Page		fc_pageHeader;
	OffsetNumber fc_offnum;

	/*
	 * 正在进行投机性插入的元组应该已经设置了其
	 * 令牌。
	 */
	Assert(!fc_token || HeapTupleHeaderIsSpeculative(fc_tuple->t_data));

	/*
	 * 不允许具有无效提示位组合的元组被放置在页面上。
	 * 此组合被 contrib/amcheck 逻辑检测为损坏，
	 * 因此，如果您禁用此断言，请在此进行相应的更改。
	 */
	Assert(!((fc_tuple->t_data->t_infomask & HEAP_XMAX_COMMITTED) &&
			 (fc_tuple->t_data->t_infomask & HEAP_XMAX_IS_MULTI)));

	/* 将元组添加到页面 */
	fc_pageHeader = BufferGetPage(fc_buffer);

	fc_offnum = PageAddItem(fc_pageHeader, (Item) fc_tuple->t_data,
						 fc_tuple->t_len, InvalidOffsetNumber, false, true);

	if (fc_offnum == InvalidOffsetNumber)
		elog(PANIC, "failed to add tuple to page");

	/* 更新 tuple->t_self 为实际存储位置 */
	ItemPointerSet(&(fc_tuple->t_self), BufferGetBlockNumber(fc_buffer), fc_offnum);

	/*
	 * 在存储的元组的 CTID 中插入正确的位置（除非
	 * 这是一次投机性插入，在这种情况下令牌保存在
	 * CTID 字段中）
	 */
	if (!fc_token)
	{
		ItemId		fc_itemId = PageGetItemId(fc_pageHeader, fc_offnum);
		HeapTupleHeader fc_item = (HeapTupleHeader) PageGetItem(fc_pageHeader, fc_itemId);

		fc_item->t_ctid = fc_tuple->t_self;
	}
}

/*
 * 以模式读取缓冲区，如果 bistate 不为 NULL，则使用批量插入策略。
 */
static Buffer fc_ReadBufferBI(Relation fc_relation, BlockNumber fc_targetBlock,
			 ReadBufferMode fc_mode, BulkInsertState fc_bistate)
{
	Buffer		fc_buffer;

	/* 如果不是批量插入，完全像 ReadBuffer 一样 */
	if (!fc_bistate)
		return ReadBufferExtended(fc_relation, MAIN_FORKNUM, fc_targetBlock,
								  fc_mode, NULL);

	/* 如果我们已经锁定了所需的块，重新锁定并返回它 */
	if (fc_bistate->current_buf != InvalidBuffer)
	{
		if (BufferGetBlockNumber(fc_bistate->current_buf) == fc_targetBlock)
		{
			/*
			 * 目前 LOCK 变种仅用于扩展
			 * 关系，永远不应该到达这个分支。
			 */
			Assert(fc_mode != RBM_ZERO_AND_LOCK &&
				   fc_mode != RBM_ZERO_AND_CLEANUP_LOCK);

			IncrBufferRefCount(fc_bistate->current_buf);
			return fc_bistate->current_buf;
		}
		/* ... 否则释放旧的缓冲区 */
		ReleaseBuffer(fc_bistate->current_buf);
		fc_bistate->current_buf = InvalidBuffer;
	}

	/* 使用缓冲区策略执行读取 */
	fc_buffer = ReadBufferExtended(fc_relation, MAIN_FORKNUM, fc_targetBlock,
								fc_mode, fc_bistate->strategy);

	/* 保存所选块作为未来插入的目标 */
	IncrBufferRefCount(fc_buffer);
	fc_bistate->current_buf = fc_buffer;

	return fc_buffer;
}

/*
 * 对于每个全可见的堆页面，如果我们还没有获得
 * 适当的可见性映射页面的锁定，则获取锁定。
 *
 * buffer2 可以是 InvalidBuffer，如果只涉及一个缓冲区。
 * buffer1 不能是 InvalidBuffer。如果指定了两个缓冲区，
 * block1 必须小于 block2。
 */
static void fc_GetVisibilityMapPins(Relation fc_relation, Buffer fc_buffer1, Buffer fc_buffer2,
					 BlockNumber fc_block1, BlockNumber fc_block2,
					 Buffer *fc_vmbuffer1, Buffer *fc_vmbuffer2)
{
	bool		fc_need_to_pin_buffer1;
	bool		fc_need_to_pin_buffer2;

	Assert(BufferIsValid(fc_buffer1));
	Assert(fc_buffer2 == InvalidBuffer || fc_block1 <= fc_block2);

	while (1)
	{
		/* 确定我们需要但没有的锁定。 */
		fc_need_to_pin_buffer1 = PageIsAllVisible(BufferGetPage(fc_buffer1))
			&& !visibilitymap_pin_ok(fc_block1, *fc_vmbuffer1);
		fc_need_to_pin_buffer2 = fc_buffer2 != InvalidBuffer
			&& PageIsAllVisible(BufferGetPage(fc_buffer2))
			&& !visibilitymap_pin_ok(fc_block2, *fc_vmbuffer2);
		if (!fc_need_to_pin_buffer1 && !fc_need_to_pin_buffer2)
			return;

		/* 在进行任何 I/O 之前我们必须解锁两个缓冲区。 */
		LockBuffer(fc_buffer1, BUFFER_LOCK_UNLOCK);
		if (fc_buffer2 != InvalidBuffer && fc_buffer2 != fc_buffer1)
			LockBuffer(fc_buffer2, BUFFER_LOCK_UNLOCK);

		/* 获取锁定。 */
		if (fc_need_to_pin_buffer1)
			visibilitymap_pin(fc_relation, fc_block1, fc_vmbuffer1);
		if (fc_need_to_pin_buffer2)
			visibilitymap_pin(fc_relation, fc_block2, fc_vmbuffer2);

		/* 重新锁定缓冲区。 */
		LockBuffer(fc_buffer1, BUFFER_LOCK_EXCLUSIVE);
		if (fc_buffer2 != InvalidBuffer && fc_buffer2 != fc_buffer1)
			LockBuffer(fc_buffer2, BUFFER_LOCK_EXCLUSIVE);

		/*
		 * 如果涉及两个缓冲区并且我们只锁定其中一个，
		 * 那么在我们忙于锁定第一个时，第二个可能已经变得全可见。
		 * 如果这看起来是一个可能的情况，我们需要在这个循环中进行第二次遍历。
		 */
		if (fc_buffer2 == InvalidBuffer || fc_buffer1 == fc_buffer2
			|| (fc_need_to_pin_buffer1 && fc_need_to_pin_buffer2))
			break;
	}
}

/*
 * 通过多个块扩展关系，以避免未来对
 * 关系扩展锁的争用。我们的目标是预先扩展关系，
 * 数量随着争用程度的增加而增加，但将结果限制在
 * 合理的总体值内。
 */
static void fc_RelationAddExtraBlocks(Relation fc_relation, BulkInsertState fc_bistate)
{
	BlockNumber fc_blockNum,
				fc_firstBlock = InvalidBlockNumber;
	int			fc_extraBlocks;
	int			fc_lockWaiters;

	/* 使用锁等待队列的长度来判断扩展的数量。 */
	fc_lockWaiters = RelationExtensionLockWaiterCount(fc_relation);
	if (fc_lockWaiters <= 0)
		return;

	/*
	 * 乘以锁等待者的数量达到 20 似乎过于激进，
	 * 但基准测试显示较小的数字不足。512 只是一个
	 * 任意上限，以防止病态结果。
	 */
	fc_extraBlocks = Min(512, fc_lockWaiters * 20);

	do
	{
		Buffer		fc_buffer;
		Page		fc_page;
		Size		fc_freespace;

		/*
		 * 扩展一个页面。这通常应与RelationGetBufferForTuple中的主线扩展代码相匹配，除了我们在整个过程中持有关系扩展锁，并且我们并不立即初始化页面（见下文）。
		 */
		fc_buffer = fc_ReadBufferBI(fc_relation, P_NEW, RBM_ZERO_AND_LOCK, fc_bistate);
		fc_page = BufferGetPage(fc_buffer);

		if (!PageIsNew(fc_page))
			elog(ERROR, "page %u of relation \"%s\" should be empty but is not",
				 BufferGetBlockNumber(fc_buffer),
				 RelationGetRelationName(fc_relation));

		/*
		 * 在不初始化的情况下将页面添加到FSM。如果我们在这里初始化，页面可能会在我们添加任何有用内容之前被刷新到磁盘。在潜在崩溃之前这样的情况没有保证，所以我们仍然需要处理未初始化的页面，因此避免不必要的写入的潜在性。
		 */

		/* 我们在下面需要这个信息 */
		fc_blockNum = BufferGetBlockNumber(fc_buffer);
		fc_freespace = BufferGetPageSize(fc_buffer) - SizeOfPageHeaderData;

		UnlockReleaseBuffer(fc_buffer);

		/* 记住因此添加的第一个块编号。 */
		if (fc_firstBlock == InvalidBlockNumber)
			fc_firstBlock = fc_blockNum;

		/*
		 * 立即更新FSM的底层。这很可能使这个页面对其他同时插入的后台可见，我们希望这一切毫不延迟地发生。
		 */
		RecordPageWithFreeSpace(fc_relation, fc_blockNum, fc_freespace);
	}
	while (--fc_extraBlocks > 0);

	/*
	 * 更新空闲空间图的上层对于每个块来说代价太高，但在结束时做一次是值得的，以确保后续的插入活动看到我们刚插入的所有那些漂亮的空闲页面。
	 */
	FreeSpaceMapVacuumRange(fc_relation, fc_firstBlock, fc_blockNum + 1);
}

/*
 * RelationGetBufferForTuple
 *
 * 返回给定关系中一个页面的固定和排他锁缓冲区，其自由空间 >= 给定长度。
 *
 * 如果 otherBuffer 不是 InvalidBuffer，则它引用同一关系中另一页面的先前固定缓冲区；返回时，该缓冲区也将被排他锁定。 (这个情况被 heap_update 使用；otherBuffer 包含被更新的元组。)
 *
 * 传递 otherBuffer 的原因是，如果两个后端正在执行并发的 heap_update 操作，如果它们试图按照相反的顺序锁定同两个缓冲区，可能会发生死锁。 为了确保这不会发生，我们规定关系的缓冲区必须按页面编号递增顺序锁定。 这通过让 RelationGetBufferForTuple 同时锁定它们来方便地完成，注意顺序。
 *
 * 注意：虽然不太可能，但 otherBuffer 也可能是我们选择用于插入新元组的相同缓冲区（这种情况仅在 heap_update 发现该页面没有足够空间后空间被释放时发生）。 在这种情况下，页面将被固定和锁定一次。
 *
 * 我们还处理了一个或两个页面的 all-visible 标志需要被清除的可能性。 如果是这样，必须在获取缓冲区锁之前确保在相关的可见性映射页面上获取固定，以避免在持有缓冲区锁时可能进行I/O操作。 固定通过输入输出参数 vmbuffer 和 vmbuffer_other 传回给调用者。 注意，在某些情况下，调用者可能已经获取了这样的固定，这通过这些参数在进入时不是 InvalidBuffer 来指示。
 *
 * 我们通常使用 FSM 来帮助我们找到自由空间。 然而，如果指定了 HEAP_INSERT_SKIP_FSM，当元组无法适应当前目标页面时，我们只是将一个新的空页面附加到关系的末尾。 当我们知道关系是新的并且不包含有用的自由空间时，这可以节省一些周期。
 *
 * HEAP_INSERT_SKIP_FSM 对于非 WAL 记录的关系添加也很有用，如果调用者持有排他锁并小心在第一次插入之前使关系的 smgr_targblock 无效——这确保所有插入将发生在新添加的页面中，而不会与其他事务的元组混合。 这样，崩溃就不会影响丢失任何其他事务已提交的数据。 （有关安全使用此行为所需的附加约束，请参见 heap_insert 的注释。）
 *
 * 调用者还可以提供一个 BulkInsertState 对象，以优化多次插入同一关系。 这会在当前插入目标页面上保持一个固定（以节省固定/解固定周期），并且还将 BULKWRITE 缓冲区选择策略对象传递给缓冲区管理器。 传递 NULL 作为 bistate 选择默认行为。
 *
 * 我们不会在现有页面填充超过 fillfactor，除了在几乎为空的页面中的大元组。 这是可以的，因为在更新元组并将其保留在同一页面上时不会调用此例程，而这是 fillfactor 意在保留空间的场景。
 *
 * 这里允许 ereport(ERROR)，因此必须在缓冲池中进行任何（未记录的）更改之前调用此例程。
 */
Buffer RelationGetBufferForTuple(Relation fc_relation, Size fc_len,
						  Buffer fc_otherBuffer, int fc_options,
						  BulkInsertState fc_bistate,
						  Buffer *fc_vmbuffer, Buffer *fc_vmbuffer_other)
{
	bool		fc_use_fsm = !(fc_options & HEAP_INSERT_SKIP_FSM);
	Buffer		fc_buffer = InvalidBuffer;
	Page		fc_page;
	Size		fc_nearlyEmptyFreeSpace,
				fc_pageFreeSpace = 0,
				fc_saveFreeSpace = 0,
				fc_targetFreeSpace = 0;
	BlockNumber fc_targetBlock,
				fc_otherBlock;
	bool		fc_needLock;

	fc_len = MAXALIGN(fc_len);		/* 保持保守 */

	/* 不支持批量插入更新，仅支持插入。 */
	Assert(fc_otherBuffer == InvalidBuffer || !fc_bistate);

	/*
	 * 如果我们要因超大元组而失败，请立即处理
	 */
	if (fc_len > MaxHeapTupleSize)
		ereport(ERROR,
				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
				 errmsg("row is too big: size %zu, maximum size %zu",
						fc_len, MaxHeapTupleSize)));

	/* 根据 fillfactor 选项计算所需的额外空闲空间 */
	fc_saveFreeSpace = RelationGetTargetPageFreeSpace(fc_relation,
												   HEAP_DEFAULT_FILLFACTOR);

	/*
	 * 由于没有元组的页面仍然可以有行指针，因此我们在不可用空间很少时将页面视为
	 * "空"。这个阈值是相当随意的，但它应该能在将大元组插入低 fillfactor 表时防止大多数不必要的关系扩展。
	 */
	fc_nearlyEmptyFreeSpace = MaxHeapTupleSize -
		(MaxHeapTuplesPerPage / 8 * sizeof(ItemIdData));
	if (fc_len + fc_saveFreeSpace > fc_nearlyEmptyFreeSpace)
		fc_targetFreeSpace = Max(fc_len, fc_nearlyEmptyFreeSpace);
	else
		fc_targetFreeSpace = fc_len + fc_saveFreeSpace;

	if (fc_otherBuffer != InvalidBuffer)
		fc_otherBlock = BufferGetBlockNumber(fc_otherBuffer);
	else
		fc_otherBlock = InvalidBlockNumber;	/* 只是为了让编译器安静 */

	/*
	 * 我们首先尝试将元组放在我们上次插入元组的同一页面上，
	 * 这在 BulkInsertState 或 relcache 条目中缓存。如果那样不行，我们就请求
	 * 空闲空间图（Free Space Map）定位一个合适的页面。
	 * 由于 FSM 的信息可能过时，我们必须做好多次循环重试的准备。（为了确保这不是一个无限循环，
	 * 我们必须在每个证明不合适的页面上更新 FSM，以获得正确的自由空间量。）如果 FSM 没有足够空闲空间的页面记录，
	 * 我们就放弃并扩展关系。
	 *
	 * 当 use_fsm 为 false 时，我们要么将元组放到现有的目标页面上，
	 * 要么扩展关系。
	 */
	if (fc_bistate && fc_bistate->current_buf != InvalidBuffer)
		fc_targetBlock = BufferGetBlockNumber(fc_bistate->current_buf);
	else
		fc_targetBlock = RelationGetTargetBlock(fc_relation);

	if (fc_targetBlock == InvalidBlockNumber && fc_use_fsm)
	{
		/*
		 * 我们没有缓存的目标页面，因此请求 FSM 获取初始目标。
		 */
		fc_targetBlock = GetPageWithFreeSpace(fc_relation, fc_targetFreeSpace);
	}

	/*
	 * 如果 FSM 对关系一无所知，请在放弃并扩展之前尝试最后一个页面。
	 * 这可以避免在引导过程中或在最近启动的系统中发生每页面一个元组的症状。
	 */
	if (fc_targetBlock == InvalidBlockNumber)
	{
		BlockNumber fc_nblocks = RelationGetNumberOfBlocks(fc_relation);

		if (fc_nblocks > 0)
			fc_targetBlock = fc_nblocks - 1;
	}

loop:
	while (fc_targetBlock != InvalidBlockNumber)
	{
		/*
		 * 读取并独占锁定目标块，如果提供了其他块，也锁定它，
		 * 在锁定顺序和可能是同一块的情况下采取适当的措施。
		 *
		 * 如果页面级的 all-visible 标志被设置，调用者需要
		 * 清除该标志以及相应的可见性映射位。然而，
		 * 在返回时，我们将锁定缓冲区，而我们不希望在这种状态下进行任何 I/O。
		 * 因此，我们在获取锁之前检查位，并在似乎必要时固定页面。
		 * 在没有锁的情况下进行检查会存在得到错误答案的风险，
		 * 所以在获取锁后我们必须重新检查。
		 */
		if (fc_otherBuffer == InvalidBuffer)
		{
			/* 简单的情况 */
			fc_buffer = fc_ReadBufferBI(fc_relation, fc_targetBlock, RBM_NORMAL, fc_bistate);
			if (PageIsAllVisible(BufferGetPage(fc_buffer)))
				visibilitymap_pin(fc_relation, fc_targetBlock, fc_vmbuffer);

			/*
			 * 如果页面为空，固定 vmbuffer 以便稍后设置 all_frozen 位。
			 */
			if ((fc_options & HEAP_INSERT_FROZEN) &&
				(PageGetMaxOffsetNumber(BufferGetPage(fc_buffer)) == 0))
				visibilitymap_pin(fc_relation, fc_targetBlock, fc_vmbuffer);

			LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);
		}
		else if (fc_otherBlock == fc_targetBlock)
		{
			/* 也很简单的情况 */
			fc_buffer = fc_otherBuffer;
			if (PageIsAllVisible(BufferGetPage(fc_buffer)))
				visibilitymap_pin(fc_relation, fc_targetBlock, fc_vmbuffer);
			LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);
		}
		else if (fc_otherBlock < fc_targetBlock)
		{
			/* 先锁定其他缓冲区 */
			fc_buffer = ReadBuffer(fc_relation, fc_targetBlock);
			if (PageIsAllVisible(BufferGetPage(fc_buffer)))
				visibilitymap_pin(fc_relation, fc_targetBlock, fc_vmbuffer);
			LockBuffer(fc_otherBuffer, BUFFER_LOCK_EXCLUSIVE);
			LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);
		}
		else
		{
			/* 先锁定目标缓冲区 */
			fc_buffer = ReadBuffer(fc_relation, fc_targetBlock);
			if (PageIsAllVisible(BufferGetPage(fc_buffer)))
				visibilitymap_pin(fc_relation, fc_targetBlock, fc_vmbuffer);
			LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);
			LockBuffer(fc_otherBuffer, BUFFER_LOCK_EXCLUSIVE);
		}

		/*
		 * 我们现在已经将目标页面（以及其他缓冲区，如果有的话）固定
		 * 和锁定。然而，由于我们的初始 PageIsAllVisible 检查
		 * 是在获取锁之前进行的，因此结果现在可能已经过时，
		 * 无论是对于选定的牺牲缓冲区，还是对于调用者传入的
		 * 其他缓冲区。在这种情况下，我们需要放弃我们的锁，去获取
		 * 我们之前未能获取的固定（pin），并重新锁定。这是相当痛苦的，
		 * 但希望不应该经常发生。
		 *
		 * 请注意，有一种小可能性是我们没有固定上述页面，
		 * 但仍然有正确的页面被固定，无论是因为我们
		 * 已经在这个循环中进行了先前的遍历，还是因为
		 * 调用者传给我们正确的页面。
		 *
		 * 还要注意的是，在我们获取固定并重新获得缓冲区锁的
		 * 时候，某个其他后端可能已经清除了可见性映射位。
		 * 在这种情况下，我们会做一些额外的工作而没有收益，
		 * 但这并没有造成真正的伤害。
		 */
		if (fc_otherBuffer == InvalidBuffer || fc_targetBlock <= fc_otherBlock)
			fc_GetVisibilityMapPins(fc_relation, fc_buffer, fc_otherBuffer,
								 fc_targetBlock, fc_otherBlock, fc_vmbuffer,
								 fc_vmbuffer_other);
		else
			fc_GetVisibilityMapPins(fc_relation, fc_otherBuffer, fc_buffer,
								 fc_otherBlock, fc_targetBlock, fc_vmbuffer_other,
								 fc_vmbuffer);

		/*
		 * 现在我们可以检查这里是否有足够的空闲空间。如果有，
		 * 那我们就完成了。
		 */
		fc_page = BufferGetPage(fc_buffer);

		/*
		 * 如有必要初始化页面，它将很快被使用。我们可以避免
		 * 在这里弄脏缓冲区，并依赖调用者在将元组放入页面上时
		 * 做到这一点，但这样做似乎没有太大好处。
		 */
		if (PageIsNew(fc_page))
		{
			PageInit(fc_page, BufferGetPageSize(fc_buffer), 0);
			MarkBufferDirty(fc_buffer);
		}

		fc_pageFreeSpace = PageGetHeapFreeSpace(fc_page);
		if (fc_targetFreeSpace <= fc_pageFreeSpace)
		{
			/* 将此页面作为未来插入目标 */
			RelationSetTargetBlock(fc_relation, fc_targetBlock);
			return fc_buffer;
		}

		/*
		 * 空间不足，因此我们必须放弃页面锁和固定（如果
		 * 有的话），并准备去寻找其他地方。我们不在乎
		 * 解锁这两个缓冲区的顺序，因此这可以比上面的
		 * 代码稍微简单一些。
		 */
		LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
		if (fc_otherBuffer == InvalidBuffer)
			ReleaseBuffer(fc_buffer);
		else if (fc_otherBlock != fc_targetBlock)
		{
			LockBuffer(fc_otherBuffer, BUFFER_LOCK_UNLOCK);
			ReleaseBuffer(fc_buffer);
		}

		/* 没有 FSM，总是跳出循环并扩展 */
		if (!fc_use_fsm)
			break;

		/*
		 * 更新 FSM 关于此页面的状态，并请求另一个页面
		 *进行尝试。
		 */
		fc_targetBlock = RecordAndGetPageWithFreeSpace(fc_relation,
													fc_targetBlock,
													fc_pageFreeSpace,
													fc_targetFreeSpace);
	}

	/*
	 * 必须扩展关系。
	 *
	 * 我们必须使用锁以确保没有其他人同时扩展该关系，
	 * 否则我们将都尝试初始化相同的新页面。然而，对于新关系
	 * 或临时关系，我们可以跳过锁定，因为没有其他人
	 * 能够访问它们。
	 */
	fc_needLock = !RELATION_IS_LOCAL(fc_relation);

	/*
	 * 如果我们需要锁但无法立即获取，我们将考虑一次扩展多个块
	 * 以管理关系扩展锁上的竞争。然而，这只有在我们使用 FSM 时
	 * 才有道理；否则，没有意义。
	 */
	if (fc_needLock)
	{
		if (!fc_use_fsm)
			LockRelationForExtension(fc_relation, ExclusiveLock);
		else if (!ConditionalLockRelationForExtension(fc_relation, ExclusiveLock))
		{
			/* 无法立即获取锁；等待它。 */
			LockRelationForExtension(fc_relation, ExclusiveLock);

			/*
			 * 检查在我们等待锁时，是否有其他后台为我们扩展了一个块。
			 */
			fc_targetBlock = GetPageWithFreeSpace(fc_relation, fc_targetFreeSpace);

			/*
			 * 如果其他等待者已经扩展了关系，我们就不需要这样做；
			 * 只需使用现有的空闲空间。
			 */
			if (fc_targetBlock != InvalidBlockNumber)
			{
				UnlockRelationForExtension(fc_relation, ExclusiveLock);
				goto loop;
			}

			/* 是时候进行批量扩展了。 */
			fc_RelationAddExtraBlocks(fc_relation, fc_bistate);
		}
	}

	/*
	 * 除了我们上面所做的任何扩展，我们总是至少添加一个块以满足我们自己的请求。
	 *
	 * XXX 这会执行一个 lseek - 相当昂贵 - 但目前这是准确确定关系中有多少块的唯一方法。
	 * 将准确的文件长度保存在共享内存中的某个地方是否值得，而不是依赖内核为我们执行这个操作？
	 */
	fc_buffer = fc_ReadBufferBI(fc_relation, P_NEW, RBM_ZERO_AND_LOCK, fc_bistate);

	/*
	 * 我们需要初始化空的新页面。 双重检查它确实是空的（这不应该发生，但如果发生，我们不想冒着覆盖有效数据的风险）。
	 */
	fc_page = BufferGetPage(fc_buffer);

	if (!PageIsNew(fc_page))
		elog(ERROR, "page %u of relation \"%s\" should be empty but is not",
			 BufferGetBlockNumber(fc_buffer),
			 RelationGetRelationName(fc_relation));

	PageInit(fc_page, BufferGetPageSize(fc_buffer), 0);
	MarkBufferDirty(fc_buffer);

	/*
	 * 页面是空的，固定 vmbuffer 以设置 all_frozen 位。
	 */
	if (fc_options & HEAP_INSERT_FROZEN)
	{
		Assert(PageGetMaxOffsetNumber(BufferGetPage(fc_buffer)) == 0);
		visibilitymap_pin(fc_relation, BufferGetBlockNumber(fc_buffer), fc_vmbuffer);
	}

	/*
	 * 释放文件扩展锁；现在其他人可以继续扩展关系。
	 */
	if (fc_needLock)
		UnlockRelationForExtension(fc_relation, ExclusiveLock);

	/*
	 * 锁定其他缓冲区。保证它的页码会低于新页面。为了符合死锁预防规则，我们应该先锁定 otherBuffer，但那会给其他后端一个机会在我们的页面上放置元组。为了减少这种可能性，尝试有条件地锁定其他缓冲区，这很可能会成功。
	 * 否则我们需要按正确的顺序锁定缓冲区，并在此期间空间被使用时重试。
	 *
	 * 另外，我们可以在扩展关系之前获取对 otherBuffer 的锁，但那会要求在执行 IO 时保持锁定，这似乎比不太可能的重试要糟糕。
	 */
	if (fc_otherBuffer != InvalidBuffer)
	{
		Assert(fc_otherBuffer != fc_buffer);
		fc_targetBlock = BufferGetBlockNumber(fc_buffer);
		Assert(fc_targetBlock > fc_otherBlock);

		if (unlikely(!ConditionalLockBuffer(fc_otherBuffer)))
		{
			LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
			LockBuffer(fc_otherBuffer, BUFFER_LOCK_EXCLUSIVE);
			LockBuffer(fc_buffer, BUFFER_LOCK_EXCLUSIVE);
		}

		/*
		 * 因为缓冲区解锁了一段时间，所以可能（尽管不太可能）出现了 all-visible 标志被设置，或者有人占用了新页面中的可用空间。
		 * 我们可以使用 GetVisibilityMapPins 来解决第一种情况。在第二种情况下，只需从头开始重试。
		 */
		fc_GetVisibilityMapPins(fc_relation, fc_otherBuffer, fc_buffer,
							 fc_otherBlock, fc_targetBlock, fc_vmbuffer_other,
							 fc_vmbuffer);

		/*
		 * 注意，即使我们的条件锁成功，我们也必须检查可用空间，因为 GetVisibilityMapPins 可能暂时释放了目标缓冲区的锁，
		 * 以获取对 otherBuffer 的 VM pin。
		 */
		if (fc_len > PageGetHeapFreeSpace(fc_page))
		{
			LockBuffer(fc_otherBuffer, BUFFER_LOCK_UNLOCK);
			UnlockReleaseBuffer(fc_buffer);

			goto loop;
		}
	}
	else if (fc_len > PageGetHeapFreeSpace(fc_page))
	{
		/* 鉴于顶部的测试，我们不应该到这里 */
		elog(PANIC, "tuple is too big: size %zu", fc_len);
	}

	/*
	 * 记住新页面作为我们未来插入的目标。
	 *
	 * XXX 我们是否应该立即将新页面输入到自由空间映射中，还是仅在短期内保留它供此后端专用（直到 VACUUM 看到它）？
	 * 似乎取决于你是否期望当前后端进行更多插入，这在大多数情况下可能是一个不错的赌注。因此现在不要将它添加到 FSM。
	 */
	RelationSetTargetBlock(fc_relation, BufferGetBlockNumber(fc_buffer));

	return fc_buffer;
}
